package work.linruchang.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import io.github.bonigarcia.wdm.WebDriverManager;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import work.linruchang.util.other.EventProcessing;

import java.awt.image.BufferedImage;
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Web自动化工具
 * 可爬虫、测试
 *
 * @author LinRuChang
 * @version 1.0
 * @date 2022/08/31
 * @since 1.8
 **/
@Slf4j
public class EnhanceSeleniumUtil {

    /**
     * 非空检查
     *
     * @param args
     */
    protected static void checkParamValidity(Object... args) {
        if (ArrayUtil.isNotEmpty(args)) {
            Stream.of(args).peek(arg -> {
                Assert.notNull(arg);
                if (arg instanceof String) {
                    Assert.notBlank(arg.toString());
                }
            });
        }
    }

    /**
     * 浏览器执行脚本
     *
     * @param browser    浏览器
     * @param script     脚本
     * @param scriptArgs 脚本入参
     * @return
     */
    public static Object executeScript(RemoteWebDriver browser, String script, Object... scriptArgs) {
        checkParamValidity(browser, script);
        Object scriptResult = browser.executeScript(script, ObjectUtil.defaultIfNull(scriptArgs, new Object[0]));
        return scriptResult;
    }

    /**
     * 浏览器执行脚本，结果转成数字类型
     *
     * @param browser    浏览器
     * @param script     脚本
     * @param scriptArgs 脚本入参
     * @return
     */
    public static Integer executeScriptToInt(RemoteWebDriver browser, String script, Object... scriptArgs) {
        return Convert.toInt(executeScript(browser, script, scriptArgs));
    }


    /**
     * 获取元素宽度
     *
     * @param browser   浏览器
     * @param className 元素的css类名
     * @return
     */
    public static Integer getElementWidthByClassName(WebDriver browser, String className) {
        return getElementWidth(getElementByClassName(browser, className));
    }

    /**
     * 获取元素宽度
     *
     * @param webElement 目标元素
     * @return
     */
    protected static Integer getElementWidth(WebElement webElement) {
        checkParamValidity(webElement);
        return webElement.getSize().getWidth();
    }

    /**
     * 获取元素高度
     *
     * @param browser   浏览器
     * @param className 元素的css类名
     * @return
     */
    public static Integer getElementHeightByClassName(WebDriver browser, String className) {
        return getElementHeight(getElementByClassName(browser, className));
    }

    /**
     * 获取元素的高度
     *
     * @param webElement 目标元素
     * @return
     */
    protected static Integer getElementHeight(WebElement webElement) {
        checkParamValidity(webElement);
        return webElement.getSize().getHeight();
    }


    /**
     * 目标元素是否存在
     *
     * @param browser         浏览器
     * @param targetClassName 目标元素的css类名
     * @return
     */
    public static boolean existElementByClassName(WebDriver browser, String targetClassName) {
        checkParamValidity(browser, targetClassName);
        try {
            getElementByClassName(browser, targetClassName);
            return true;
        } catch (Exception e) {
            return false;
        }
    }


    /**
     * 根据className找的一系列元素，这些元素的内容需匹配targetContent的元素索引
     *
     * @param browser       浏览器
     * @param className     目标元素
     * @param targetContent 目标元素的内容
     * @return 找不到则返回-1
     */
    public static Integer getIndexByContentAndClassName(WebDriver browser, String className, String targetContent) {
        checkParamValidity(browser, className, targetContent);
        List<String> contentsByClassName = getContentsByClassName(browser, className);
        for (int index = 0; index < contentsByClassName.size(); index++) {
            if (StrUtil.equals(contentsByClassName.get(index), targetContent)) {
                return index;
            }
        }
        return -1;
    }


    /**
     * 鼠标移动到某个位置，然后进行点击操作
     *
     * @param xOffset x坐标
     * @param yOffset y坐标
     */
    public static void mouseMoveAndClick(WebDriver browser, int xOffset, int yOffset) {
        checkParamValidity(browser);
        new Actions(browser).moveByOffset(xOffset, yOffset).click().perform();
    }


    /**
     * 浏览器窗口最大化
     *
     * @param browser 浏览器
     */
    public static void maximize(WebDriver browser) {
        checkParamValidity(browser);
        browser.manage().window().maximize();
    }

    /**
     * 获取某个元素 【默认等待30s，30s仍未出现直接抛出异常】
     *
     * @param browser   浏览器
     * @param className 元素classname  注意：仅能填一个类名，某个元素定义了多个类名请使用xpath选择器
     * @return
     */
    public static WebElement getElementByClassName(WebDriver browser, String className) {
        checkParamValidity(browser, className);
        // 最多等待30s，元素仍未出来，直接抛出异常
        return getElementByWait(browser, By.ByClassName.className(className));
    }


    /**
     * 获取某个元素 【默认等待30s，30s仍未出现直接抛出异常】
     *
     * @param browser   浏览器
     * @param tagName 元素标签名
     * @return
     */
    public static WebElement getElementByTagName(WebDriver browser, String tagName) {
        checkParamValidity(browser, tagName);
        // 最多等待30s，元素仍未出来，直接抛出异常
        return getElementByWait(browser, By.ByTagName.tagName(tagName));
    }

    /**
     * 等待元素消失（最多等30秒，仍然未消失直接抛出异常），然后触发eventProcessing事件运行
     *
     * @param browser             浏览器
     * @param webElementClassName 等待消失的元素的css类名
     * @param eventProcessing     元素消息后触发的事件
     * @return
     */
    public static WebElement waitElementsDisappearByClassName(WebDriver browser, String webElementClassName, EventProcessing eventProcessing) {
        WebElement targetElement = getElementByClassName(browser, webElementClassName);
        return waitElementsDisappear(browser, targetElement, eventProcessing);
    }

    /**
     * 等待元素消失（最多等30秒，仍然未消失直接抛出异常），然后触发eventProcessing事件运行
     *
     * @param browser         浏览器
     * @param webElement      等待消失的元素
     * @param eventProcessing 元素消息后触发的事件
     * @return
     */
    protected static WebElement waitElementsDisappear(WebDriver browser, WebElement webElement, EventProcessing eventProcessing) {
        checkParamValidity(browser, webElement);
        WebDriverWait webDriverWait = new WebDriverWait(browser, Duration.ofSeconds(30));
        webDriverWait.until(ExpectedConditions.invisibilityOf(webElement));
        if (eventProcessing != null) {
            eventProcessing.process(browser, webElement);
        }
        return webElement;
    }

    /**
     * 用户可见（意外着此元素已经在DOM结构中）
     * 当前的webElement是已经存在dom结构中，但是没有显示出来而已
     *
     * @param browser    浏览器
     * @param webElement 待出现的目标元素
     * @return
     */
    protected static WebElement getElementByShow(WebDriver browser, WebElement webElement) {
        checkParamValidity(browser, webElement);
        WebDriverWait webDriverWait = new WebDriverWait(browser, Duration.ofSeconds(30));
        webDriverWait.until(ExpectedConditions.visibilityOf(webElement));
        return webElement;
    }

    /**
     * 元素必须用户可见（最多等待60s）
     *
     * @param browser
     * @param searchElementConditions
     * @return
     */
    public static WebElement getElementByWait(WebDriver browser, By searchElementConditions) {
        // 等待元素出现在Dom结构中
        WebElement targetElement = new WebDriverWait(browser, Duration.ofSeconds(30)).until(driver ->
                driver.findElement(searchElementConditions)
        );
        return getElementByShow(browser, targetElement);
    }


    /**
     * 等待容器中的每个元素直到出现为止（每个元素分别最多等30s，如果出现一个未出现则直接抛出异常）
     *
     * @param browser     浏览器
     * @param cssSelector css选择器
     * @return
     */
    public static List<WebElement> getElementsByCssSelector(WebDriver browser, String cssSelector) {
        checkParamValidity(browser, cssSelector);
        return getElementsByWait(browser, By.cssSelector(cssSelector));
    }


    /**
     * 等待容器中的每个元素直到出现为止（每个元素分别最多等30s，如果出现一个未出现则直接抛出异常）
     *
     * @param browser   浏览器
     * @param className 元素classname  注意：仅能填一个类名，某个元素定义了多个类名请使用xpath选择器
     * @return
     */
    public static List<WebElement> getElementsByClassName(WebDriver browser, String className) {
        checkParamValidity(browser, className);
        return getElementsByWait(browser, By.className(className));
    }

    /**
     * 元素需显示出来（用户可见）
     *
     * @param browser
     * @param webElements
     * @return
     */
    protected static List<WebElement> getElementsByShow(WebDriver browser, List<WebElement> webElements) {
        WebDriverWait webDriverWait = new WebDriverWait(browser, Duration.ofSeconds(30));
        return CollUtil.emptyIfNull(webElements).stream()
                .map(targetElement -> webDriverWait.until(ExpectedConditions.visibilityOf(targetElement)))
                .collect(Collectors.toList());
    }

    /**
     * 等待元素显示出来，用户可见
     * @param browser
     * @param searchElementConditions
     * @return
     */
    public static List<WebElement> getElementsByWait(WebDriver browser, By searchElementConditions) {
        checkParamValidity(browser, searchElementConditions);
        //元素出现在dom中
        List<WebElement> targetElements = new WebDriverWait(browser, Duration.ofSeconds(30)).until(driver ->
                driver.findElements(searchElementConditions)
        );
        //用户可见元素
        return getElementsByShow(browser, targetElements);
    }



    public static String getContentByClassName(WebDriver browser, String className) {
        return getContentsByClassName(browser, className).stream()
                .findFirst()
                .orElse(null);
    }

    /**
     * 获取元素的内容
     *
     * @param className
     * @return
     */
    public static List<String> getContentsByClassName(WebDriver browser, String className) {
        checkParamValidity(browser, className);
        return getContentsByWebElements(getElementsByClassName(browser, className));
    }

    /**
     * 获取元素的内容
     *
     * @param webElements 元素
     * @return
     */
    protected static List<String> getContentsByWebElements(List<WebElement> webElements) {
        return CollUtil.emptyIfNull(webElements)
                .stream()
                .map(WebElement::getText)
                .map(StrUtil::trim)
                .collect(Collectors.toList());
    }


    /**
     * 浏览器垂直滚动
     *
     * @param browser      浏览器
     * @param scrollHeight 滚动的高度
     */
    public static void scrollByVertical(WebDriver browser, Integer scrollHeight) {
        checkParamValidity(browser, scrollHeight);
        new Actions(browser).scrollByAmount(0, scrollHeight).perform();
    }

    /**
     * 点击某个元素
     * 最多等待30s，元素仍未出来，直接抛出异常
     *
     * @param browser 浏览器
     * @param xpath   xpath选择器  参考<a href="https://www.runoob.com/xpath/xpath-tutorial.html"></a>
     * @return
     */
    public static WebElement clickElementByXpath(WebDriver browser, String xpath) {
        checkParamValidity(browser, xpath);
        return clickElement(browser, By.xpath(xpath));
    }

    /**
     * 点击某个元素
     * 最多等待30s，元素仍未出来，直接抛出异常
     *
     * @param browser     浏览器
     * @param cssSelector css选择器  参考<a href="https://www.runoob.com/cssref/css-ref-aural.html"></a>
     * @return
     */
    public static WebElement clickElementByCssSelector(WebDriver browser, String cssSelector) {
        checkParamValidity(browser, cssSelector);
        return clickElement(browser, By.cssSelector(cssSelector));
    }

    /**
     * 点击某个元素
     * 最多等待30s，元素仍未出来，直接抛出异常
     *
     * @param browser   浏览器
     * @param className 元素classname  注意：仅能填一个类名，某个元素定义了多个类名请使用xpath选择器
     * @return
     */
    public static WebElement clickElementByClassName(WebDriver browser, String className) {
        checkParamValidity(browser, className);
        return clickElement(browser, By.className(className));
    }

    public static WebElement clickElement(WebDriver browser, By searchElementConditions) {
        return clickElement(browser, getElementByWait(browser, searchElementConditions));
    }

    /**
     * 点击某个元素
     * 最多等待30s，元素仍未出来，直接抛出异常
     *
     * @param browser       浏览器
     * @param targetElement 目标元素
     * @return
     */
    protected static WebElement clickElement(WebDriver browser, WebElement targetElement) {
        checkParamValidity(browser, targetElement);
        // 元素出现，触发鼠标点击
        new Actions(browser).click(targetElement).perform();
        return targetElement;
    }


    /**
     * 爬取（页面截图在然后OCR识别）：https://www.likecs.com/show-304558.html
     *
     * @param args
     */
    @SneakyThrows
    public static void main(String[] args) {
        WebDriverManager.chromedriver().setup();
        ChromeDriver driver = new ChromeDriver();
        driver.get("https://weread.qq.com/web/reader/b4c3255071bc6bdeb4c5a20ka87322c014a87ff679a21ea");
        driver.manage().window().maximize();

        // 获取文章标题
        // WebElement titleContentElement = driver.findElement(By.xpath("//div[@class='chapter-header']/h1"));
        // String titleContent = titleContentElement.getText();
        // System.out.println(titleContent);
        // Thread.sleep(10000);


        Integer pageHeight = Convert.toInt(driver.executeScript("return document.documentElement.scrollHeight", new Object[0]));
        Console.log("页面高度：{}", pageHeight);

        // Integer windowHeight = driver.manage().window().getSize().getHeight();
        Integer windowHeight = Convert.toInt(driver.executeScript("return window.innerHeight", new Object[0]));
        Console.log("窗口高度：{}", windowHeight);


        BufferedImage sumImage = null;
        // for(int i =0; i<((pageHeight%windowHeight ==0)?pageHeight/windowHeight:pageHeight/windowHeight+1 ); i++ ){
        for (int i = 0; i < 3; i++) {
            Console.log("滚动开始{}", i);
            File screenshotAs = driver.getScreenshotAs(OutputType.FILE);
            File currentDirScreenshot = new File(StrUtil.format("C:\\Users\\Administrator\\Desktop\\截图\\截图{}.png", i));
            FileUtil.copy(screenshotAs, currentDirScreenshot, true);
            BufferedImage currentScreenshot = EnhanceImgUtil.read(currentDirScreenshot);
            sumImage = EnhanceImgUtil.mergeImage(false, sumImage, currentScreenshot);
            new Actions(driver).scrollByAmount(0, windowHeight).perform();
            Console.log("合并截图");
            Console.log("滚动结束{}", i);
            Thread.sleep(1000);
        }

        EnhanceImgUtil.write(sumImage, new File("C:\\Users\\Administrator\\Desktop\\截图.png"));


        // 页面截图
        // File screenshotAs = driver.getScreenshotAs(OutputType.FILE);
        // FileUtil.copy(screenshotAs,new File("C:\\Users\\Administrator\\Desktop\\截图.png"), true);

        // 页面内容
        // String pageSource = driver.getPageSource();
        // Console.log(pageSource);


        // 关闭浏览器
        // driver.close();

    }


}
